package il.ac.technion.cs236700.address_book;


import il.ac.technion.cs236700.address_book.Defaults.ESUPPORTED_KEYS;
import il.ac.technion.cs236700.utils.Misc;
import il.ac.technion.cs236700.utils.MyClonable;
import il.ac.technion.cs236700.utils.CSV.Separator;
import il.ac.technion.cs236700.utils.DBC.DBC;
import il.ac.technion.cs236700.utils.annotations.Getter;
import il.ac.technion.cs236700.utils.annotations.Setter;
import il.ac.technion.cs236700.utils.visitor.Action;
import il.ac.technion.cs236700.utils.visitor.SimpleVisitor;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/** 
 * 
 * @authors Regev Azriel & Sharon Ronen
 *
 */

public final class Contact implements MyClonable<Contact>, Comparable<Contact>{
	
	/**
	 * mapping for the keys of the contacts and the values of it
	 * (e.g <"First Name","Josh">) 
	 */
	private Map<String,String> map;

	public Contact() {
		map = new HashMap<String, String>();
	}
	
	public Contact(Map<String,String> values) {
		this.map = Misc.cloneMap(values);
	}

	/**
	 * Default implementation for {@link Contact} conflicting:
	 * consolidate-able: Two records can be consolidated if they have the same
     * first and last name, and they agree on all other keys.
	 * Conflict: Two records are in conflict if they have the same first and
	 * last name, at least one key where the corresponding values are not
	 * in agreement with each other.
	 * Agreement: Two values (of the same key) "agree" with each other if one of
	 * them is equal to- or is a substring of- the other. An empty string is a 
	 * substring of every string (this definition was not changed </p>
	 * 
	 * @param c2 - A contact to compare with
	 * @param supportedKeys - The set of keys that is used for determining conflicts
	 * if supportedKeys contains a key that does't exist in this.map nor in c2.map
	 * this method throws exception
	 * @return true/false according the default rules above 
	 */
	public boolean conflictsWith(Contact c2){
		return equalInitials(c2) && !agreeWith(c2);
	}

	/**
	 * Default implementation for {@link Contact} unification:
	 * consolidate-able: Two records can be consolidated if they have the same
     * first and last name, and they agree on all other keys.
     * Agreement: Two values (of the same key) "agree" with each other if one of
	 * them is equal to- or is a substring of- the other. An empty string is a 
	 * substring of every string (this definition was not changed 
     */
	public boolean canUnify(Contact c2) {
		return !equals(c2) && equalInitials(c2) && agreeWith(c2);
	}

	private boolean equalInitials(Contact c2) {
		return this.FirstName().equals(c2.FirstName()) && this.LastName().equals(c2.LastName());
	}

	private boolean agreeWith(Contact c2) {
		Map<String,String> m2 = c2.getMap();
		String v;
		for (String k : m2.keySet()) {
			if ((v = map.get(k))!= null && 
					!(v.contains(m2.get(k))||m2.get(k).contains(v)))
				return false;
		}
		return true;
	}

	/**
	 * after ensuring the this can be unifying with c2,
	 * adding the additional info c2 holds to this
	 * @param c2 - contact with additional info
	 */
	public void unify(Contact c2) {
		DBC.require(this.canUnify(c2), "Contacts cannot be unified");
		final Map<String,String> m2 = c2.getMap();		
		Collection<String> keys = new ArrayList<String>(m2.keySet());
		new SimpleVisitor<String>(keys,new Action<String>(){
			@Override
			public void visit(String key) {
				if (map.get(key) == null || m2.get(key).contains(map.get(key))) 
					map.put(key, m2.get(key));				
			}		
		}).go();
	}

	/**
	 * 
	 * @param p - a person's details
	 * @return true if the contact has (at least) the same details as p holds
	 */
	public boolean contains(Person p) {
		String firstName = map.get(Defaults.getValue(ESUPPORTED_KEYS.FirstName));
		String lastName = map.get(Defaults.getValue(ESUPPORTED_KEYS.LastName));
		String title = map.get(Defaults.getValue(ESUPPORTED_KEYS.Title));
		DBC.require(firstName!=null && lastName!=null, "Contact must have first and last name");
		return firstName.equals(p.firstName)&& lastName.equals(p.lastName)&&(title==null || title.equals(p.title));
	}
	
	/**
	 * 
	 * @param outputFields - the keys (and order) to display
	 * @return a CSV string for the values in the contact's map
	 * 			the order of the string is according to the order of the keys in outputFields
	 */
	public String output(ArrayList<String> outputFields) {
		String value= "";
		ArrayList<String> values = new ArrayList<String>(); 
		for (String key : outputFields) {
			if ((value = map.get(key))!=null) values.add(value);
			else values.add("");
		}
		return Separator.separateBy(values,",");		
	}
	
	/**
	 * 
	 * @return the string "Last Name"
	 */
	public String LastName() {
		return map.get(Defaults.ESUPPORTED_KEYS.LastName.displayLable());
	}
	
	/**
	 * 
	 * @return the string "First Name"
	 */
	public String FirstName() {
		return map.get(Defaults.ESUPPORTED_KEYS.FirstName.displayLable());
	}
	
	/**
	 * 
	 * @return copy of the map
	 */
	@Getter(Defensive=true)
	public Map<String, String> getMapCopy() {
		return Misc.cloneMap(map);
	}
	
	/**
	 * 
	 * @return the map itself
	 */
	@Getter(Defensive=false)
	public Map<String, String> getMap(){
		return map;
	}
	
	/**
	 * Creating copy of map from the the specified map
	 * @param values
	 */
	@Setter(Defensive=true)
	public void setValues(Map<String, String> values) {
		this.map = Misc.cloneMap(values);
	}
	
	/**
	 * Overriding the equal method 
	 */
	@Override
	public boolean equals(Object obj) {				
		if (!(obj instanceof Contact)) return false;
		return map.equals(((Contact)obj).map);
	}
	
	/** 
	 * Overriding the method hashCode
	 */
	@Override
	public int hashCode() {
		return Misc.hash(map.values());
	}
	
	/**
	 * Overriding the method toSting
	 */
	@Override
	public String toString() {
		return output(new ArrayList<String>(map.keySet()));
	}
	
	/**
	 * Overriding the method clone
	 */
	@Override
	public Contact clone() {
		return new Contact(map);
	}

	/**
	 * setting the first name of this contact
	 * @param firstName
	 */
	public void setFirstName(String firstName) {
		setNewEntry(Defaults.getValue(ESUPPORTED_KEYS.FirstName),firstName);		
	}
	
	/**
	 * Setting the last name of this contact
	 * @param lastName
	 */
	public void setLastName(String lastName) {
		setNewEntry(Defaults.getValue(ESUPPORTED_KEYS.LastName),lastName);		
	}

	/**
	 * adding new entry to the contact's map
	 * if the contact already has an entry with key - overrides it
	 * @param key
	 * @param value
	 */
	public void setNewEntry(String key, String value) {
		if (map.containsKey(key)) map.remove(key);
		map.put(key, value);		
	}

	/**
	 * 
	 * @param key - if the contact doesn't have such entry - stops
	 */
	public void removeEntry(String key){
		DBC.require(map.containsKey(key), "when removing - map must contains the key");
		map.remove(key);
	}
	
	/**
	 * 
	 * @return an output string with all the (key,value) pairs according to the 
	 * (inner) order of the keys in this contact
	 */
	public String output() {
		return this.toString();
	}

	@Override
	/**
	 * Compare first using last name ,if equals - compare first name
	 */
	public int compareTo(Contact o) {
		int _ = this.LastName().compareTo(o.LastName());
		if (_==0)
			return this.FirstName().compareTo(o.FirstName());
		return _;
	}
	 /**
	  * Adding  all of the mappings from the specified map to this map 
	  * to the map of this contact
	  * @param values
	  */
	public void addToMap(Map<String, String> values){
		this.map.putAll(values);
	}
	
	public void setValue(Map<String, String> values){
		Map<String, String> $ = this.getMapCopy();
		for (String key : this.map.keySet()) {
			for (String valueKey : values.keySet()) {
				if (key.equals(valueKey)) {
					$ .remove(key);
					$ .put(valueKey,values.get(valueKey));
				}
			}
		}
		this.setValues($);
	}
	
	/**
	 * Checking if all of the mappings from the specified map contain
	 * in the map of this contact
	 * @param values
	 * @return
	 */
	public boolean containMap(Map<String, String> values){
		for(String key: values.keySet()){
			if(map.containsKey(key)){
				if(!map.containsValue(values.get(key))){
					System.out.println(key);
					return false;
				}
			}
			else return false;
		}
		return true;
	}
}
